Skip to content

When the sheet name is specified as $Property$, it auto splits into multiple sheets if the property is enumerable, Supports template for nested objects #959

Draft
iyumot wants to merge 2 commits into
mini-software:masterfrom
FundOffice:extent
Draft

When the sheet name is specified as $Property$, it auto splits into multiple sheets if the property is enumerable, Supports template for nested objects #959
iyumot wants to merge 2 commits into
mini-software:masterfrom
FundOffice:extent

Conversation

@iyumot
Copy link
Copy Markdown

@iyumot iyumot commented May 15, 2026

with template

image image

model
`
public record struct Identity(int Type, string Id);

private class Fund
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public Identity Identity { get; set; }
    public DateOnly SetupDate { get; set; }

    public List<NetValue> NetValues { get; set; } = [];
}

public record NetValue(DateOnly Date, decimal Value);

`

with data

    var value = new
    {
        Funds = fundList.Select(x => new
        {
            x.Id,
            x.Name,
            x.Identity,
            x.SetupDate,
            x.NetValues,
            SheetName = x.Name
        })
    };

specify sheetname by SheetName property, if not exist set as what in $$ plus 1,2,3

result
image
image
image

k1k-xxsc added 2 commits May 15, 2026 09:10
{{Funds.Id}} {{Funds.Name}} {{Funds.Identity.Id}} {{Funds.Identity.Type}} {{Funds.SetupDate.Year}}
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces multi-sheet expansion capabilities and enhanced object flattening with circular reference protection to the OpenXml template engine. The reviewer identified several critical bugs, including a double increment of the sheet index that breaks file relationships and a loop termination issue that prevents processing multiple template sheets. Additionally, feedback was provided regarding redundant type checks, inefficient regex instantiation, unnecessary decimal parsing, and missing namespace imports required for cross-platform compilation.

Comment on lines +184 to +196
sheetIdx++;
var newSheetPath = $"xl/worksheets/sheet{sheetIdx}.xml";

// 处理表名
string finalSheetName;
if (subValues.TryGetValue("SheetName", out var customSheetName) && customSheetName != null)
{
finalSheetName = customSheetName.ToString()?.Trim() ?? $"{baseSheetName}{subIndex++}";
}
else
{
finalSheetName = $"{baseSheetName}{sheetIdx++}";
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The sheetIdx variable is incremented twice within the loop: once at line 184 for the file path and again at line 195 for the default sheet name. This results in a mismatch between the generated XML file names (e.g., sheet1.xml) and the indices stored in allSheetInfos (e.g., 2), which will cause Excel to fail when opening the file because the relationship targets in workbook.xml will point to non-existent files. Additionally, subIndex is defined but not used in the default naming logic.

                sheetIdx++;
                var newSheetPath = $"xl/worksheets/sheet{sheetIdx}.xml";

                // 处理表名
                string finalSheetName;
                if (subValues.TryGetValue("SheetName", out var customSheetName) && customSheetName != null)
                {
                    finalSheetName = customSheetName.ToString()?.Trim() ?? $"{baseSheetName}{subIndex++}";
                }
                else
                {
                    finalSheetName = $"{baseSheetName}{subIndex++}";
                }

Comment on lines +56 to +60
if (value == null || value.GetType() is not Type type)
{
replacements[key] = string.Empty;
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The pattern matching is not Type type is redundant. If value is not null, value.GetType() always returns a Type instance. This can be simplified to improve readability.

        if (value == null)
        {
            replacements[key] = string.Empty;
            return;
        }

        var type = value.GetType();

Comment on lines +1 to +2
using System.ComponentModel;
using System.Xml.Linq;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This new file is missing several required using statements for types used in the code, such as System.Text.RegularExpressions (for Regex), System.Reflection (for PropertyInfo), System.Collections (for IEnumerable), System.Globalization (for CultureInfo), and System.Runtime.CompilerServices (for RuntimeHelpers). Since the project targets multiple frameworks including those without global usings, these should be explicitly imported to ensure compilation across all targets.


private async Task<bool> HookSheetProcess(OpenXmlZip outputFileArchive, string realSheetName, IDictionary<int, string> templateSharedStrings, int sheetIdx, List<(int Index, string Name)> allSheetInfos, ZipArchiveEntry templateSheet, string templateFullName, IDictionary<string, object?> inputValues, CancellationToken cancellationToken)
{
var m = Regex.Match(realSheetName, @"\$([^$]+)\$");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Creating a new Regex instance on every call to HookSheetProcess is inefficient. Since this is a constant pattern, it should be defined as a static compiled field. Additionally, realSheetName should be checked for null before calling Regex.Match to avoid a ArgumentNullException if the sheet name mapping fails.

Comment on lines +139 to +143
if (TypeHelper.IsNumericType(type))
{
if (decimal.TryParse(cellValueStr, out var decimalValue))
cellValueStr = decimalValue.ToString(CultureInfo.InvariantCulture);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Parsing cellValueStr back to a decimal just to call ToString(CultureInfo.InvariantCulture) is redundant and inefficient. cellValueStr was already obtained from the object. If the goal is to ensure invariant culture formatting for numeric types, it should be applied directly when converting the original numeric value to a string.

Comment on lines +138 to +139
if (await HookSheetProcess(outputFileArchive, realSheetName, templateSharedStrings, sheetIdx, allSheetInfos, templateSheet, templateFullName, inputValues, cancellationToken).ConfigureAwait(false))
break;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Breaking the loop when HookSheetProcess returns true prevents any subsequent template sheets from being processed. If a template contains multiple sheets and one of them uses the expansion logic, all sheets following it will be missing from the output file. Furthermore, sheetIdx is passed by value, so the caller's index is not updated after expansion, which would cause file name conflicts if the loop were to continue.

@michelebastione michelebastione marked this pull request as draft May 15, 2026 06:44
@michelebastione
Copy link
Copy Markdown
Collaborator

Thank you for your contribution, I will try to evaluate it as soon as possible. Please change the title and description to include a more detailed description of the intents of the PR.

@iyumot iyumot changed the title Extent When the sheet name is specified as $Property$, it auto splits into multiple sheets if the property is enumerable, Supports template binding for nested objects May 15, 2026
@iyumot iyumot changed the title When the sheet name is specified as $Property$, it auto splits into multiple sheets if the property is enumerable, Supports template binding for nested objects When the sheet name is specified as $Property$, it auto splits into multiple sheets if the property is enumerable, Supports template for nested objects May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants